Skip to content

Refactor: Block Editor#35257

Merged
rjvelazco merged 103 commits into
mainfrom
refactor-dotcms-block-editor
May 8, 2026
Merged

Refactor: Block Editor#35257
rjvelazco merged 103 commits into
mainfrom
refactor-dotcms-block-editor

Conversation

@rjvelazco
Copy link
Copy Markdown
Member

@rjvelazco rjvelazco commented Apr 8, 2026

🎯 What's this PR about?

This PR introduces a completely refactored Block Editor built on top of TipTap v3 with a modern Angular standalone architecture. The goal: make the editor faster, easier to maintain, and easier to extend — while keeping the existing user experience and content format intact.

Videos

video.mov
video.mov
feature-flag.mov

✨ Features

Everything users love about the Block Editor — now rebuilt with a cleaner foundation:

  • 📝 Rich text editing — bold, italic, underline, strikethrough, headings, lists, code, blockquote
  • Slash menu (/) — quickly insert blocks, content types, and contentlets without leaving the keyboard
  • 🎨 Floating toolbar — context-aware formatting that follows the selection
  • 🔗 Link, image, video & table popovers — anchored to the caret, keyboard friendly
  • 🤖 AI integration — Ask AI for content + AI-generated images (centered modals)
  • 😀 Emoji picker powered by emoji-mart
  • 📦 Drag handle gutter — reorder blocks visually
  • 📤 Drag-and-drop uploads — images and videos with inline upload placeholders
  • 🧩 dotCMS contentlet blocks — embed and edit contentlets directly inside the editor
  • 🎯 Allowed blocks / content types — restrict what authors can insert per field

🚦 Rollout & rollback safety — feature flag

The new editor ships behind a feature flag so existing customer instances cannot be broken by an unexpected regression. Both editors are bundled together for this transition window; the JSP that renders the field picks which custom element to mount based on the flag.

  • Flag name: FEATURE_FLAG_NEW_BLOCK_EDITOR (follows the standard FEATURE_FLAG_* prefix convention)
  • Default value: false → the legacy editor renders for every customer until they explicitly opt in
  • Where it lives:
    • dotmarketing-config.properties — default false
    • FeatureFlagName.java — Java constant
    • ConfigurationResource.java — exposed to the frontend via the config endpoint whitelist
    • FeaturedFlags enum in @dotcms/dotcms-models — referenced by the three Angular consumers (dot-edit-content-block-editor, dot-block-editor-sidebar, dot-content-compare-block-editor)
    • edit_field.jspConfigUtils.isFeatureFlagOn("FEATURE_FLAG_NEW_BLOCK_EDITOR") selects between <dotcms-block-editor> (new) and <dotcms-old-block-editor> (legacy)
  • How to enable per instance: flip FEATURE_FLAG_NEW_BLOCK_EDITOR=true in the dotCMS config or set it via the admin properties UI. No restart required.
  • Cleanup runbook: core-web/libs/new-block-editor/CLEANUP_FEATURE_FLAG.md — step-by-step instructions for removing the flag, deleting the legacy lib, renaming new-block-editorblock-editor, and dropping tippy.js once QA signs off.

The legacy libs/block-editor/ is intentionally kept on this branch as the rollback target. It will be deleted in a follow-up PR once the new editor exits QA.


🔄 Backwards compatibility

No data migration required. Existing block-editor content keeps working as-is.

  • Same JSON content format — the editor reads/writes the same TipTap document shape that the legacy editor produced. Any content already saved in your database will load and render correctly.
  • Same field configurationallowedBlocks and allowedContentTypes field variables behave exactly like before.
  • Drop-in replacement at the field level — the <dotcms-block-editor> web component keeps the same inputs/outputs/JSON shape. The field-rendering JSP picks between <dotcms-block-editor> (new) and <dotcms-old-block-editor> (legacy) based on the feature flag, so the customer-facing field stays the same regardless of which editor renders.
  • 🔁 Portlet integration shim — Edit Content (dot-edit-content-block-editor), EMA (dot-block-editor-sidebar), and Content Compare (dot-content-compare-block-editor) each gained a small flag-driven @if/@else that mounts the new or legacy editor. Public inputs/outputs of those portlet components are unchanged. The shim is removed by the cleanup runbook once QA signs off.

🛠 Changes

Apps

  • apps/dotcms-block-editor — migrated from NgModule bootstrap to Angular standalone (bootstrapApplication). Registers the new <dotcms-block-editor> custom element from @dotcms/new-block-editor, and also registers the legacy <dotcms-old-block-editor> custom element (from the preserved @dotcms/block-editor lib) so the field-rendering JSP can swap between the two via the feature flag. The legacy registration block is removed by the cleanup runbook once QA signs off.

Libraries

  • libs/new-block-editor — brand new library that hosts the refactored editor, its extensions, services, and UI.
  • libs/block-editorkept on this branch as the rollback target. No new feature work goes here; it's marked @deprecated and slated for deletion in the cleanup PR.

Dependencies

  • ⬆️ TipTap v3 + ngx-tiptap upgraded to latest majors
  • Floating UI for popover positioning
  • emoji-mart for the emoji picker
  • lowlight + @tiptap/extension-code-block-lowlight for code-block syntax highlighting (paired with a custom Angular node view that renders a PrimeNG language picker)

Styling

  • New Tailwind layers + typography plugin tuned for the editor surface
  • TipTap, table, link, and upload-placeholder styles consolidated in styles.css
  • Added Material Symbols font for editor icons
  • Removed an unused shared SCSS code block

Reuses existing data-access services

The new editor does not duplicate HTTP plumbing. It consumes services already in @dotcms/data-access:

  • DotContentTypeService, DotContentSearchService — slash menu content pickers
  • DotAiService — AI content & image generation
  • DotUploadFileService — temp upload + workflow PUBLISH
  • DotLanguagesService, DotMessageService — language metadata & i18n

Editor-specific concerns (state, popovers, modals, slash menu orchestration, contentlet-edit URL caching) live as small local services inside the lib.


🏗 New project structure

libs/new-block-editor/
└── src/lib/editor/
    ├── editor.component.ts             # Top-level <dotcms-block-editor> shell
    ├── editor.utils.ts                 # Shared helpers
    ├── config.utils.ts                 # Editor config builder
    │
    ├── components/                     # UI pieces
    │   ├── toolbar/                    # Floating formatting toolbar
    │   ├── slash-menu/                 # "/" command menu (catalog + service + UI)
    │   ├── editor-popover.component    # Anchored popover shell
    │   ├── link-popover.component
    │   ├── image-popover.component
    │   ├── table-popover.component
    │   ├── ai-content-dialog           # Centered AI modal
    │   └── emoji-picker.component
    │
    ├── extensions/                     # TipTap extensions
    │   ├── nodes/                      # Custom nodes
    │   │   ├── contentlet/             # dotCMS contentlet block
    │   │   ├── image.extension.ts
    │   │   ├── video.extension.ts
    │   │   ├── ai-content.extension.ts
    │   │   ├── grid.extension.ts
    │   │   └── upload-placeholder.extension.ts
    │   ├── link.extension.ts
    │   ├── slash-command.extension.ts
    │   ├── block-gutter.extension.ts   # Drag-handle gutter
    │   └── selection-preserve.extension.ts
    │
    ├── services/                       # Editor-only services
    │   ├── editor-popover.service.ts   # Popover open/close state
    │   ├── editor-modal.service.ts     # Centered modal orchestration
    │   ├── dot-upload.service.ts       # Async adapter over DotUploadFileService
    │   └── contentlet-edit-url.service.ts
    │
    ├── store/                          # NgRx Signals store
    │   ├── editor.store.ts             # Language, allowed blocks, AI status
    │   └── editor-block-types.ts
    │
    └── utils/                          # Stateless helpers
        ├── markdown.utils.ts
        ├── breadcrumb.utils.ts
        └── remote-extensions.loader.ts

Architectural rule of thumb:

🟦 Local services — state & orchestration stay in the lib
🟩 Data-access services — HTTP & persistence live in @dotcms/data-access


🧪 How to verify

  1. Open any content type with a Block Editor field → start typing.
  2. Type / → confirm the slash menu opens, lists allowed blocks, and the content-type sub-picker loads contentlets.
  3. Try the floating toolbar with no selection (click Bold on an empty line) — the active state should reflect immediately.
  4. Drag-and-drop an image and a video onto the editor → both should upload and insert.
  5. Open Ask AI and AI Image from the slash menu → both modals should generate and insert content.
  6. Click an inserted contentlet block → the Edit Contentlet action should resolve via the cached URL.
  7. Open an existing piece of block-editor content saved with the old editor → it should load unchanged.
  8. Flip FEATURE_FLAG_NEW_BLOCK_EDITOR=true and reload — the new <dotcms-block-editor> element should mount instead of <dotcms-old-block-editor>. Toggle back to false and confirm the legacy editor renders again with the same content intact.

Note

High Risk
High risk because this is a large refactor that swaps the block editor app to a new standalone editor implementation, upgrades TipTap/ngx-tiptap major versions, and adds dotCMS upload/search integrations including a hardcoded auth token/base URL in code.

Overview
Replaces the dotcms-block-editor app's NgModule-based bootstrap with Angular standalone bootstrapApplication and points the app at the new EditorComponent exported from @dotcms/new-block-editor, alongside updated Angular build target config (new executor/outputPath structure, dev config, baseHref).

Adds a new new-block-editor library implementing an experimental TipTap v3-based editor with slash menu, toolbar, drag-handle gutter, link/image/video/table dialogs, emoji picker, upload placeholders, and dotCMS-backed asset/content-type search + upload services.

Updates global styling for the new editor UI (Tailwind layers, typography plugin, Material Symbols, TipTap/table/link/upload-placeholder styles) and bumps editor-related dependencies (TipTap v3, ngx-tiptap, Floating UI, emoji-mart); also removes a shared SCSS code style block.

Reviewed by Cursor Bugbot for commit 77fd1d1. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

❌ Issue Linking Required

This PR could not be linked to an issue. All PRs must be linked to an issue for tracking purposes.

How to fix this:

Option 1: Add keyword to PR body (Recommended - auto-removes this comment)
Edit this PR description and add one of these lines:

  • This PR fixes #123 or Fixes: #123

  • This PR closes #123 or Closes: #123

  • This PR resolves #123 or Resolves: #123

  • Other supported keywords: fix, fixed, close, closed, resolve, resolved
    Option 2: Link via GitHub UI (Note: won't clear the failed check)

  1. Go to the PR → Development section (right sidebar)

  2. Click "Link issue" and select an existing issue

  3. Push a new commit or re-run the workflow to clear the failed check
    Option 3: Use branch naming
    Create a new branch with one of these patterns:

  • 123-feature-description (number at start)

  • issue-123-feature-description (issue-number at start)

  • feature-issue-123 (issue-number anywhere)

Why is this required?

Issue linking ensures proper tracking, documentation, and helps maintain project history. It connects your code changes to the problem they solve.---

This comment was automatically generated by the issue linking workflow

@github-actions github-actions Bot added the Area : Frontend PR changes Angular/TypeScript frontend code label Apr 8, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 8, 2026

Claude finished @rjvelazco's task in 5m 2s —— View job


PR Review — Block Editor Refactor (synchronize re-review)

Re-reviewed by Claude Opus 4.7 (1M). Confirms what previous review items were resolved and surfaces new issues since the last pass. Skipping praise per request.


Resolved since prior review

  • L2 (feature-flag default-true on NOT_FOUND) — fixed. All three consumers now call getFeatureFlagWithDefault(FEATURE_FLAG_NEW_BLOCK_EDITOR, false). getFeatureFlagWithDefault also lowercases for env-driven "True"/"TRUE". ✅
  • AI extension "dead code" (prior L1) — was a misread. The aiContent node is intentionally registered for legacy storage compat (see CLAUDE.md "TipTap Node Names Are Immutable"). The new editor inserts AI HTML as standard nodes via commands.insertContent. The comment in ai-content.extension.ts:5-12 and editor-extensions.ts:116-122 now make that explicit. ✅
  • Allowed-block name mismatch — the catalog and has(...) now share the same vocabulary (heading1..6, bulletList, dotContent, codeBlock, link, emoji, gridBlock, etc.) and headings expose all six levels via StarterKit.heading.levels. ✅
  • Hardcoded JWT, demo feature.md, leftover app.config/routes/spec — all gone. ✅
  • Link validatorlinkHrefValidator now accepts mailto:, tel:, anchors (#x), and relative paths (/page, ./page, ../page). ✅

Still unresolved from prior review

  • Tests are still effectively zero in libs/new-block-editor. find libs/new-block-editor -name '*.spec.ts' returns nothing. jest.config.ts and tsconfig.spec.json are wired but every service / store / extension / CVA path is untested. Below the bar set in core-web/CLAUDE.md.
  • editor.component.ts:482-484 fullscreen seed effect — still re-runs every input change. If a host re-emits [isFullscreen]="false" mid-session, it exits fullscreen the user just entered. Seed once or use untracked.
  • Material Symbols .woff2 (~3.8 MB) — bundled into dotcms-ui for ~50 ligatures. Subset to the icons actually used (undo, format_bold, …) before this becomes the default.
  • Legacy code style removed from shared libs/dotcms-scss/angular/styles.scss — Cursor Bugbot's flag is still valid. The rule was global; now nothing replaces it outside the editor's scoped editor.component.css. Either re-scope the new style or restore the global one.
  • tippy.js still in core-web/package.json — even though the new editor uses Floating UI instead. CLEANUP runbook says it'll come out at flag removal time, but it's still being shipped today.

New issues since prior review

# Severity Where Issue
N1 High services/contentlet-edit-url.service.ts:47 const useNew = !!ct?.metadata?.[FEATURE_FLAG_CONTENT_EDITOR2_ENABLED]; — same env-var-string bug getFeatureFlagWithDefault was just patched for. If the metadata returns the string "false" (env-driven config), !!"false" is true and the editor routes to the new content editor when the customer wants legacy. Apply the same value.toLowerCase() === 'true' coercion for strings, or pull a shared helper. Fix this →
N2 Medium services/editor-modal.service.ts:146-147 Stale doc comment. Says "inserts the generated HTML as an aiContent node" but the implementation at :171 is editor.chain().focus().insertContent(html).run() — inserts as paragraphs/headings/lists. Aligns with the rest of the codebase but contradicts the comment.
N3 Medium editor.utils.ts:62-65, 91-94 Drop-upload failures still go to console.error + silent placeholder removal — the user sees their dropped file vanish with no feedback. Use DotHttpErrorManagerService or a toast. Same point was raised in prior review and not addressed.
N4 Medium extensions/nodes/upload-placeholder.extension.ts renderHTML is defined but no parseHTML and the node isn't marked transient. If the user saves the document while an upload is in flight, the resulting JSON contains a uploadPlaceholder node permanently. Either block save while in-flight uploads exist, or strip these nodes from editor.getJSON() before emitting valueChange.
N5 Medium components/toolbar/toolbar.component.ts:515-523 if (window.parent) is always truthy — for a top-level window, window.parent === window. So the standalone editor (dev or future embed) navigates its own window away, losing unsaved state mid-edit. Use if (window.parent !== window) and fall back to window.location (or skip navigation) for top-level. The ConfirmationService gates the action, but the user's intent ("open the contentlet in the parent admin shell") doesn't match what happens.
N6 Low extensions/editor-extensions.ts:1, :64 lowlight + common (~150 language definitions) is imported eagerly at module load. Inside the has('codeBlock') branch the cost is justified — but for fields where codeBlock is disabled, the bundle still pays for it. Move the dynamic import inside the branch so it tree-shakes. Same applies to Emoji + emojis (~700 KB) — eagerly imported even when has('emoji') is false.
N7 Low extensions/editor-extensions.ts:55-63 Headings 4-6 are reachable through allowedBlocks (heading4, heading5, heading6) but never exposed in the slash-menu catalog or toolbar block-type select. Customers who set allowedBlocks="heading4" will get a working extension but no way for authors to insert it. Either expose 4-6 in the UI or document that headings are clamped to 1-3 by design. (Dario asked the same question in his review thread — still unanswered in code.)
N8 Low services/contentlet-edit-url.service.ts:39-50 Cache lives forever and is keyed only on content-type variable. If a customer flips FEATURE_FLAG_CONTENT_EDITOR2_ENABLED mid-session, the editor sticks with the cached value until reload. Also: a transient 4xx caches false (legacy URL) for the rest of the session, even after the backend recovers. Consider a short TTL or skip-cache-on-error.
N9 Low store/editor.store.ts:122-127 loadAiInstalled is one-shot. If the AI service hiccups on the first load, the slash menu hides AI entries for the rest of the session. The aiInstalled signal stays null. A retry or a re-trigger on slash-menu open would be cheap.
N10 Low services/editor-popover.service.ts:72-73 TODO comment about merging openImageProperties / openLink into a generic open(id, payload, fn) is still there. Now that there are exactly three popover-with-payload openers (image-properties, link, asset-by-url uses none), this is the right time to do it — three is the threshold the original "wait for a third caller" rule targets.
N11 Low editor.utils.ts:39-43 Placeholder ids use Date.now() + index. Two simultaneous drops within the same ms (separate drops, not files within one drop) can collide. Use crypto.randomUUID() or include a random suffix.
N12 Low apps/dotcms-block-editor/src/main.ts:36-55 wireDevDemo() reads EDITOR_DEMO_CONTENT and assigns it to a #demo element when !environment.production. Fine for local dev — but the demo content (editor-demo-content.ts is now committed at the app level) should not be loaded into the production bundle. Verify environment.production is true in the customer build, otherwise the 41 KB demo content ships to production.

Other observations (no change requested)

  • dot-block-editor selector vs dot-old-block-editor — renamed cleanly so there's no Angular selector collision while the flag-shim is live. <dotcms-block-editor> and <dotcms-old-block-editor> custom elements are also distinct. Good.
  • Link extension defaults (rel: 'noopener noreferrer', target: '_self') plus linkHrefValidator covering http/https/mailto/tel/anchor/relative — no obvious open-redirect / script-injection vectors.
  • AI content dialog sanitizes via Angular's DomSanitizer once and re-uses the result for both [innerHTML] preview and dialogRef.close(html) — clean.
  • Upload service stays same-origin via cookie auth (withCredentials: true is implicit through DotUploadFileService); no token in code.
  • Legacy block-editor lib is @deprecated and rollback target — confirmed via the cleanup runbook. The shim adds three duplicated @if (isNewBlockEditorEnabled()) blocks across consumers — small surface, easy to delete after QA.

Recommendation

Merge-blocking: N1 (env-var string-flag bug in contentlet-edit-url.service). Strongly recommended before flag flip: N3, N4, N5 + at least a smoke-test pass on the CVA contract, store, and EditorModalService. Everything else can land as follow-ups under the cleanup runbook.


  • Pull current branch state and diff against origin/main
  • Verify which previous review items have been resolved
  • Read changed files for new issues
  • Compile review focused on what's wrong/risky/worth a second look
    • Branch: refactor-dotcms-block-editor

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 8, 2026

Rollback Safety Analysis - Safe to Roll Back. All 9 changed files are frontend Angular config only (new-block-editor library scaffold). Label AI: Safe To Rollback applied.

@rjvelazco rjvelazco mentioned this pull request Apr 8, 2026
5 tasks
@rjvelazco rjvelazco linked an issue Apr 8, 2026 that may be closed by this pull request
5 tasks
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

Bugbot Autofix is kicking off a free cloud agent to fix these issues. This run is complimentary, but you can enable autofix for all future PRs in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 77fd1d1. Configure here.

Comment thread core-web/libs/new-block-editor/src/lib/editor/services/dot-cms.config.ts Outdated
Comment thread core-web/libs/new-block-editor/src/lib/app.config.ts Outdated
Comment thread core-web/libs/dotcms-scss/angular/styles.scss
…o dialogs, enhancing search and display functionality
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 10, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 10, 2026

Rollback Safety Analysis

  • Read rollback-unsafe categories reference
  • Analyze PR diff against unsafe categories
  • Post results and apply labels

View job run

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 10, 2026

Rollback Safety Analysis - Safe to Roll Back. All 57 changed files are frontend Angular/TypeScript only (new-block-editor library scaffold, block-editor app refactor, SCSS, package.json). No database migrations, Elasticsearch mapping changes, API contract changes, or any backend code modified. Label AI: Safe To Rollback applied.

@semgrep-code-dotcms-test
Copy link
Copy Markdown
Contributor

Legal Risk

The following dependencies were released under a license that
has been flagged by your organization for consideration.

Recommendation

While merging is not directly blocked, it's best to pause and consider what it means to use this license before continuing. If you are unsure, reach out to your security team or Semgrep admin to address this issue.

GPL-2.0

MPL-2.0

…proved store management, and updated slash menu functionality

- Added a comprehensive porting checklist for tracking features in the new block editor.
- Introduced EditorStore for managing editor state, including allowed block types and language ID.
- Updated slash menu service to utilize the new store for block type filtering.
- Refactored image and video extensions to use consistent node names.
- Improved editor component with additional input properties for better integration with dotCMS content types.
- Cleaned up unused code and comments in editor click handling.

This commit enhances the overall functionality and maintainability of the new block editor.
Copy link
Copy Markdown
Member

@zJaaal zJaaal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is behind a FF so we can iterate over with no issues

Comment thread core-web/libs/new-block-editor/TODO.md Outdated
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Components were switched to getFeatureFlagWithDefault for explicit
NOT_FOUND defaulting, but the specs still mocked only the legacy
getFeatureFlag. toSignal(undefined) crashed in the constructor —
surfaced in CI as a misleading "HTMLCanvasElement.getContext"
warning. Adds the missing method to all four affected mocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ServiceMock

Same regression as the previous fix, different surface. The shared
`dotPropertiesServiceMock` in `libs/portlets/edit-ema/portlet/src/lib/shared/mocks.ts`
is consumed by `edit-ema-editor.component.spec`, `dot-ema-shell.component.spec`,
and `dot-uve.store.integration.spec`. None of them stub
`getFeatureFlagWithDefault`, so the editor's transitive child
DotBlockEditorSidebarComponent crashed in its constructor with
`toSignal(undefined)`. Adds the method (plus `getFeatureFlag` for
consistency) to the shared mock; covers all three callers in one place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Block Editor v2

3 participants